import java.io.*;
import java.net.*;
import java.util.*;
import java.math.*;
import java.lang.*;
import org.xml.sax.*;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import org.xml.sax.helpers.XMLReaderFactory;
import java.security.SignatureException;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.MessageDigest;
import java.util.regex.*;

class ServiceInterface implements Runnable {
	private String node_id = "1001";
	private String node_url = "director.ecs.soton.ac.uk:8452";
	
	private String url_base = "director.ecs.soton.ac.uk:8452";
	private Socket client;
	private String log_file = "log/status.log_" + getDate();
	private Hashtable request_ht = new Hashtable();
	private Hashtable response_ht = new Hashtable();
	private String message = "";	
	private String base_path = "data/";

	ServiceInterface(Socket client) {
		this.client = client;
	}

	private String getDate() {
		DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
		Date date = new Date();
		return dateFormat.format(date);
	}

	private String getDateTime() {
		SimpleDateFormat format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z");
		format.setCalendar(Calendar.getInstance(new SimpleTimeZone(0, "GMT")));
		return format.format(new Date());
		
		//DateFormat dateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss");
		//Date date = new Date();
		//return dateFormat.format(date) + " GMT";
	}

	public void run(){
		//InputStream in = null;
		BufferedReader in = null;
		OutputStream out = null;
		BufferedWriter log_writer = null;
		try{
			client.setKeepAlive(true);
			log_writer = new BufferedWriter(new FileWriter(log_file,true));
			//in = client.getInputStream();
			out = client.getOutputStream();
			// Keep the buffer external
			in = new BufferedReader(new InputStreamReader(client.getInputStream()));
			//out = new PrintWriter(client.getOutputStream(), true);
		} catch (IOException e) {
			try {
				System.out.println("Connection Closed 0");
				log_writer.write("Failed to establish input and output streams."); log_writer.newLine();
				client.close();
			} catch (IOException ex) {
				System.out.println(ex);
				ex.printStackTrace();
			}
		}
		boolean processor = true;
		while (processor) {
			processor = process_single_request(in,out,log_writer);
		}
	}
	
	private boolean process_single_request(BufferedReader in, OutputStream out, BufferedWriter log_writer) {
		String line = "";
		request_ht = new Hashtable();
		response_ht = new Hashtable();
		Vector header_lines = new Vector();
		
		request_ht.put("content-length",0);
		header_lines = read_header(in,log_writer);
		int http_code = process_header(header_lines);
		String string_to_sign = "";
		if (http_code > 399) {
			try {
				int status = header_message(http_code,out);
				client.close();	
				return false;
			} catch (IOException s_error) {	
				s_error.printStackTrace();
			}
		}
		String type = (String)request_ht.get("type"); 
		String auth_temp_amz = (String)request_ht.get("authorization");
		if (auth_temp_amz == null) {
			message = "MissingSecurityHeader: Your request was missing a required header.";
			http_code = 400;
		} else {
			http_code = authorize_request();
		}
		if (http_code > 399) {
			try {
				int status = header_message(http_code,out);
				client.close();	
				return false;
			} catch (IOException s_error) {	
				s_error.printStackTrace();
			}
		} else {
			int status = 0;
			if (type.equals("PUT") || (type.equals("POST"))) {	

				String clustername="This Computer";
				try {

				// Read POST Content
				// We assume that the header was read above and the BufferedReader is pointing to the content 

				String action = in.readLine();

				//System.out.println(action);

				// Need to modify TRANSCODE so it would use the second protocol
				if (action.equals("TRANSCODE")) {
					logAndPrint("TRANSCODE REQUEST", log_writer);

					// Read file name and output formats lines

					//String fn_line = in.readLine();
					//String of_line = in.readLine();

					String input_name = in.readLine();
					String out_formats = in.readLine();

					/* Dont think we need this. Leaving it just as reference to regex method
					// Extract arguments

					// Change pattern to search for illegal characters
					Pattern fn_p = Pattern.compile("filename: (.+)");
					Matcher fn_m = fn_p.matcher(fn_line);
					if (fn_m.find()) { input_name = fn_m.group(1); }
					else {
						logAndPrint("Error: Input file name not specified.", log_writer);
						message += "ERROR: Input file name not specified.\n";
						header_message(400,out);
						System.exit(1); }
					
					Pattern of_p = Pattern.compile("out_formats: (.+)");
					Matcher of_m = of_p.matcher(of_line);
					if (of_m.find()) { out_formats = of_m.group(1); }
					else { 
						logAndPrint("Error: Output format not specified.", log_writer);
						message += "ERROR: Output format not specified.";
                        	                header_message(400,out);                                        
						System.exit(1); }
					*/


					// Filtering script parameters
					// Called script also filters the arguments just in case. If changing the pattern, care to change in the script too. 

					input_name = input_name.replaceAll("[\\|;\"'`\\\\]","");
					out_formats = out_formats.replaceAll("[^\\w|&|:|\\.|\\-]","");

					// Generate conversions script

					String cmd = "/Users/ecstv/fctranscode/conv.pl \""+input_name+"\" \""+out_formats+"\"";

					// Run script
				
					logAndPrint("Command:"+cmd, log_writer);
					
					Process child = Runtime.getRuntime().exec(cmd);

					// Print script output
					BufferedReader child_output = new BufferedReader (new InputStreamReader(child.getInputStream()));
					String ch_line;
					while ((ch_line = child_output.readLine()) != null) {
						message += ch_line;
					}
					http_code = 202;
				}
				else if (action.equals("MONITOR")) {
					logAndPrint("MONITOR REQUEST", log_writer);

					String remote_ids = in.readLine();

					String cmd = "/Users/ecstv/fctranscode/monitor.pl \""+remote_ids+"\"";
					logAndPrint("Command:"+cmd, log_writer);

					Process child = Runtime.getRuntime().exec(cmd);

					// Print output
                                        BufferedReader child_output = new BufferedReader (new InputStreamReader(child.getInputStream()));
                                        String ch_line;
                                        while ((ch_line = child_output.readLine()) != null) {
						//logAndPrint(ch_line, log_writer);
						message += ch_line+"\n"; }

					http_code = 202;
				}
				else if (action.equals("KILL")) {
					logAndPrint("KILL REQUEST", log_writer);

					String remote_id = in.readLine();

                	                // Filtering script parameters
                        	        // Called script also filters the arguments just in case. If changing the pattern, care to change in the script too.
					// NEEDS TO CREATE FILTER FOR REMOTE_ID!!!

	                                String cmd = "/Users/ecstv/fctranscode/kill.pl "+remote_id;
        	                        logAndPrint("Command: "+cmd, log_writer);
                	                Process child = Runtime.getRuntime().exec(cmd);

                        	        // Print kill script output
                                	BufferedReader child_output = new BufferedReader (new InputStreamReader(child.getInputStream()));
                                        String ch_line;
	                                while ((ch_line = child_output.readLine()) != null) message += ch_line+"\n";

        	                        http_code = 202;

				}
				else if (action.equals("FINISH_ACK")) {
                                        logAndPrint("FINISH_ACK REQUEST", log_writer);

                                        String remote_ids = in.readLine();

                                        String cmd = "/Users/ecstv/fctranscode/finish_ack.pl \""+remote_ids+"\"";
                                        logAndPrint("Command:"+cmd, log_writer);

                                        Process child = Runtime.getRuntime().exec(cmd);

                                        // Print output
                                        BufferedReader child_output = new BufferedReader (new InputStreamReader(child.getInputStream()));
                                        String ch_line;
                                        while ((ch_line = child_output.readLine()) != null) {
                                                //logAndPrint(ch_line, log_writer);
                                                message += ch_line+"\n"; }

                                        http_code = 202;
                                }
				else if (action.equals("DURATION")) {
					logAndPrint("DURATION REQUEST", log_writer);

                                        String filename = in.readLine();

					filename = filename.replaceAll("[\\|;\"'`\\\\]","");

                                        String cmd = "/Users/ecstv/fctranscode/duration.pl \""+filename+"\"";
                                        //logAndPrint("Command:"+cmd, log_writer);

                                        Process child = Runtime.getRuntime().exec(cmd);

                                        // Print output
                                        BufferedReader child_output = new BufferedReader (new InputStreamReader(child.getInputStream()));
                                        String ch_line;
                                        while ((ch_line = child_output.readLine()) != null) {
                                                //logAndPrint(ch_line, log_writer);
                                                message += ch_line+"\n"; }

                                        http_code = 202;
				}
				else {
					logAndPrint("Invalid action: "+action, log_writer);
					message += "ERROR: Invalid action:"+action;
					http_code=400;
				}
					header_message(http_code,out);

				} catch (IOException ioe) { System.out.println(ioe); }
				
			} else if (type.equals("GET")) {
			} else if (type.equals("HEAD")) {
			} else if (type.equals("DELETE")) {
			}
		}
		try {
			client.close();	
			log_writer.close();
			return false;
		} catch (IOException s_error) {	
			s_error.printStackTrace();
		}
		return true;
	}
	
private String get_requested_path() {
	String host_part = (String)request_ht.get("host");
	host_part = host_part.replace(url_base,"");
	try {
		if (host_part.indexOf(":") > 0) {
			host_part = host_part.substring(0,host_part.indexOf(":"));
		}
		if (host_part.substring(host_part.length()-1,host_part.length()).equals(".")) {
			host_part = host_part.substring(0,host_part.length()-1);
		}
		if (!host_part.equals("")) {
			host_part = "/" + host_part;
		}
	} catch (Exception e) {
	}
	String uri = (String)request_ht.get("uri");
	if (uri.indexOf("?") > -1 && (uri.indexOf("=") > uri.indexOf("?"))) {
		uri = uri.substring(0,uri.indexOf("?"));
	}
	return host_part + uri;
}

public int authorize_request() 	{
	String string_to_sign="";
	Hashtable amz_values = new Hashtable();
	Vector amz_keys = new Vector();
	Enumeration keys = request_ht.keys();
	while ( keys.hasMoreElements() )
	{
		String key = (String)keys.nextElement();
		if (key.length() > 4) {
			String lkey = key.toLowerCase();
			if (lkey.substring(0,5).equals("x-amz")) {
				amz_keys.add(lkey);
				amz_values.put(lkey,(String)request_ht.get(key));
			}
		}
	}
	Collections.sort(amz_keys);

	try {
		String type = (String)request_ht.get("type");
		string_to_sign += type + "\n";
		if (type.equals("GET") || type.equals("HEAD")) {
			string_to_sign += "\n\n";
			string_to_sign += (String)request_ht.get("date") + "\n";
		} else if (type.equals("PUT") || type.equals("POST")) {
			if (request_ht.containsKey("content-md5")) {
				string_to_sign += (String)request_ht.get("content-md5") + "\n";
			} else {
				string_to_sign += "\n";
			}
			if (request_ht.containsKey("content-type")) {
				string_to_sign += (String)request_ht.get("content-type") + "\n";
			} else {
				string_to_sign += "\n";
			}
			string_to_sign += (String)request_ht.get("date") + "\n";
			for(int i=0; i<amz_keys.size(); i++) {
				String local_key = ((String)amz_keys.get(i));
				string_to_sign += local_key + ":" + (String)amz_values.get(local_key) + "\n";
			}
		} else if (type.equals("DELETE")) {
			string_to_sign += "\n";
			string_to_sign += "\n";
			if ((String)amz_values.get("x-amz-date") == null) {
				string_to_sign += (String)request_ht.get("date") + "\n";
			} else {
				string_to_sign += "\n";
				string_to_sign += "x-amz-date:" + (String)amz_values.get("x-amz-date") + "\n";
			}
		}

		String requested_path = get_requested_path();
		string_to_sign += requested_path;

		String aws_access_string = (String)request_ht.get("authorization");
		String[] parts = aws_access_string.split(" ");
		aws_access_string = parts[1];
		parts = aws_access_string.split(":");
		String aws_access_id = parts[0];
		String aws_signature = parts[1];
		
		//int http_code = dbm.userExists(aws_access_id);
		//Check aws_access_id is valid (exists)
		//if (http_code > 399) {
		//	message = "InvalidAccessKeyId: The AWS Access Key Id you provided does not exist in our records. (" + aws_access_id + ")";
		//	return 403;
		//}
		request_ht.put("access_id",aws_access_id);

		//String aws_private_key = dbm.getPrivateKey(aws_access_id);
		//String aws_private_key = "fdshjfdsgjkftqsaeurbweiubt587b846926y9";
		String aws_private_key = "test_key";
		if (aws_private_key.equals("500")) {
			return 500;
		}
		System.out.println("<string_to_sign>");
		System.out.println(string_to_sign);
		System.out.println("</string_to_sign>");
		String our_sign = calculateRFC2104HMAC(string_to_sign,aws_private_key);
		if (our_sign.equals(aws_signature)) {
			return 100;
		} else {
			System.out.println("Yours : " + aws_signature + " OURS " + our_sign + "\n" + string_to_sign);
			message = "SignatureDoesNotMatch: The request signature we calculated does not match the signature you provided. Check your AWS Secret Access Key and signing method. For more information, see Authenticating REST Requests and Authenticating SOAP Requests for details.";
			return 403;
		} 
	} catch (Exception s2serror) {
		s2serror.printStackTrace();
		return 400;
	}
}
	public Vector read_header(BufferedReader in, BufferedWriter log_writer) {
		
		
		String line = "foo";
		Vector header_lines = new Vector();
		int chars = 0;
		try {
			line = in.readLine();
		} catch (Exception e) {
			e.printStackTrace();
		}
		try {
			log_writer.write("Processing Request from " + client.getInetAddress().toString() + " (" + getDateTime() + ")"); log_writer.newLine();
			line = line.trim();
		} catch (IOException e) {
			System.out.println("FAILED");
			e.printStackTrace();
			try {
				System.out.println("Connection Closed 1");
				log_writer.write("Read from " + client.getInetAddress().toString() +" at " + getDateTime() + " failed"); log_writer.newLine();
				client.close();
			} catch (IOException ex) {
			}
		}
		while(!line.equals("")){
			try{
				System.out.println("Request Line: " + line);
				header_lines.add(line);
				log_writer.write(line); log_writer.newLine();
				line = in.readLine();
			} catch (IOException e) {
				try {
					System.out.println("Connection Closed 2");
					log_writer.write("Read from " + client.getInetAddress().toString() +" at " + getDateTime() + " failed"); log_writer.newLine();
					client.close();
					System.exit(0);
				} catch (IOException ex) {
				}
			}
		}
		//try {
		//	in.close();	
		//} catch (Exception e) {
		//	e.printStackTrace();
		//}
		return header_lines;
	}

	private int process_header(Vector header_lines) {
		String line = (String)header_lines.get(0);
		String[] request = line.split(" ");
		if (request[0].equalsIgnoreCase("HEAD") || request[0].equalsIgnoreCase("GET") || request[0].equalsIgnoreCase("POST") || request[0].equalsIgnoreCase("PUT") || request[0].equalsIgnoreCase("DELETE") || request[0].equalsIgnoreCase("TRACE") || request[0].equalsIgnoreCase("OPTIONS") || request[0].equalsIgnoreCase("CONNECT")) {
			request_ht.put("type",request[0]);
			String uri = request[1];
			if (uri.indexOf("?") > 0) {
				String uri_params = uri.substring(uri.indexOf("?")+1,uri.length());
				String[] uri_params_bits = uri_params.split("&");
				String aws_access_id = "aws_user";
				String aws_signature = "";
				String expires = "";
				for (int i=0; i<uri_params_bits.length; i++) {
					String[] sides = uri_params_bits[i].split("=");
					if (sides[0].equals("AWSAccessKeyId")) {
						aws_access_id = sides[1];
					}
					if (sides[0].equals("Signature")) {
						aws_signature = new URLDecoder().decode(sides[1]);
					}
					if (sides[0].equals("Expires")) {
						expires = sides[1];
					}
				}
				request_ht.put("authorization","AWS " + aws_access_id + ":" + aws_signature);
				request_ht.put("date",expires);

			}
			request_ht.put("uri",uri);
			request_ht.put("protocal",request[2]);
			Enumeration e = header_lines.elements();
			e.nextElement();
			while (e.hasMoreElements()) {
				line = (String)e.nextElement();
				request = line.split(":",2);
				String to_put = "";
				String req_key = request[0].trim().toLowerCase();
				try {
					to_put = (String)request_ht.get(req_key);
					if (to_put != null) {
						to_put += ",";
					}
				} catch (Exception not_existant) {}
				if (to_put != null ) {
					to_put += request[1].trim();
				} else {
					to_put = request[1].trim();
				}
				request_ht.put(req_key,to_put);
			}
			return 200;
		} else {
			System.out.println("Line process error");
			message = "Bad Request: Could not understand headers";
			return 400;
		}
	}

	private static final String HMAC_SHA1_ALGORITHM = "HmacSHA1";


	/**
	 * Computes RFC 2104-compliant HMAC signature.
	 * * @param data
	 * The data to be signed.
	 * @param key
	 * The signing key.
	 * @return
	 * The Base64-encoded RFC 2104-compliant HMAC signature.
	 * @throws
	 * java.security.SignatureException when signature generation fails
	 */
	public static String calculateRFC2104HMAC(String data, String key)
		throws java.security.SignatureException
		{
			String result;
			try {

				// get an hmac_sha1 key from the raw key bytes
				SecretKeySpec signingKey = new SecretKeySpec(key.getBytes(), HMAC_SHA1_ALGORITHM);

				// get an hmac_sha1 Mac instance and initialize with the signing key
				Mac mac = Mac.getInstance(HMAC_SHA1_ALGORITHM);
				mac.init(signingKey);

				// compute the hmac on input data bytes
				byte[] rawHmac = mac.doFinal(data.getBytes());

				// base64-encode the hmac
				result = Encoding.EncodeBase64(rawHmac);

			} catch (Exception e) {
				throw new SignatureException("Failed to generate HMAC : " + e.getMessage());
			}
			return result;
		}



	private int header_message(int http_code, OutputStream ops) {
		PrintStream out = new PrintStream(ops);
		switch (http_code) {
			case 100: out.println("HTTP/1.1 100 Continue"); outputResponse(100,out); break;
			case 200: out.println("HTTP/1.1 200 OK"); break;
			case 202: out.println("HTTP/1.1 202 Accepted"); outputResponse(202,out); break;
			case 204: out.println("HTTP/1.1 204 No Content"); outputResponse(204,out); break;
			case 302: out.println("HTTP/1.1 302 Found"); outputResponse(302,out); break;
			case 307: out.println("HTTP/1.1 307 Temporary Redirect"); outputResponse(307,out); break;
			case 400: out.println("HTTP/1.1 400 Bad Request"); outputResponse(400,out); break;
			case 403: out.println("HTTP/1.1 403 Forbidden"); outputResponse(403,out); break;
			case 404: out.println("HTTP/1.1 404 Not Found"); outputResponse(404,out); break;
			case 409: out.println("HTTP/1.1 409 Conflict"); outputResponse(409,out); break;
			case 415: out.println("HTTP/1.1 415 Unsupported Media Type"); outputResponse(415,out); break;
			case 500: out.println("HTTP/1.1 500 Internal Server Error"); outputResponse(500,out); break;
			default: out.println("HTTP/1.1 400 Bad Request"); break;
		}
		//out.println("Date: " + getDateTime());
		//out.println("Server: Service Controller");
		//out.println("X-Powered-By: Java");
		//out.println("Connection: close");
		//out.println("Content-Type: text/xml; charset=utf-8");
		out.println("");
		return http_code;
	}
	private void outputResponse(int http_code,PrintStream out) {
		
		out.println("Date: " + getDateTime());
		out.println("Server: Service Controller");
		out.println("X-Powered-By: Java");
		out.println("Connection: close");
		if (http_code == 302 || http_code == 307) {
			out.println("Location: " + response_ht.get("location"));
		}
		if (!message.equals("")) {
			out.println("Content-Type: text/html; charset=utf-8");
			out.println("Content-Length: " + message.toCharArray().length);
			out.println("");
			out.println(message);
		} else {
			out.println("Content-Length: 0");
		}
	}
	private void logAndPrint(String log_mes, BufferedWriter log_writer){
		try {
		Date now = new Date();
		System.out.println(now+": "+log_mes);
		log_writer.write(now+": "+log_mes); log_writer.newLine();

		} catch(IOException ioe) { System.out.println(ioe); } 
	}
	private void http_ret(String mess){

		System.out.println(mess);
		message += mess+"\n";
	}
}

class SocketThrdServer {

	ServerSocket server = null;

	SocketThrdServer(){
		/**
		 * Constructor
		 * In here you can do things like construct a UI or something, but since each thread will handle things differently we don't want a master interfacce.
		 */
	} 

	public void listenSocket(int port){
		try{
			server = new ServerSocket(port);
			System.out.println("Listening on port " + port);
		} catch (IOException e) {
			System.out.println("Could not listen on port " + port);
			System.exit(-1);
		}
		while(true){
			ServiceInterface si;
			try{
				si = new ServiceInterface(server.accept()) ;
				Thread sit = new Thread(si);
				sit.start();
			} catch (IOException e) {
				System.out.println("Accept failed: " + port);
				System.exit(-1);
			}
		}
	}

	protected void finalize(){
		//Objects created in run method are finalized when 
		//program terminates and thread exits
		try{
			server.close();
		} catch (IOException e) {
			System.out.println("Could not close socket");
			System.exit(-1);
		}
	}

	public static void main(String[] args){
		int port = 8452;
		try {
			if (args[0].equals("--help")) {
				System.out.println("Service Interface Listener (v0.1nea)");
				System.out.println("");
				System.out.println("Usage:");
				System.out.println("");
				System.out.println(" -p    Port Number to Listen On");
				System.out.println("");
				System.out.println("");
				System.exit(0);
			} else {
				while (args.length > 0) {
					if (args[0].equals("-p")) {
						port = Integer.parseInt(args[1]);
					}
					if (args.length > 2) {
						String[] foo = new String[args.length-2];
						for (int i=2;i<args.length;i++) {
							foo[i-2] = args[i];
						}
						args = foo;
					} else {
						args = new String[0];
					}
				}
			}
		} catch ( Exception e) {
		}

		try {

		// Run script to check if necessary programs and directories available
		System.out.println("Checking required folders and programs.");

		Process exists_check_child = Runtime.getRuntime().exec("/Users/ecstv/fctranscode/exists_check.pl");

		BufferedReader child_output = new BufferedReader (new InputStreamReader(child.getInputStream()));
                String ch_line;
                while ((ch_line = child_output.readLine()) != null) {
                	System.out.println(ch_line);
                }
		// TODO - If exists_check.pl returns any error, this program should exit 

		// Run script to check for transcoding that were lost on a system restart/crash
		System.out.println("Checking for transcodings that were lost on a system restart/crash.");

		Process restart_check_child = Runtime.getRuntime().exec("/Users/ecstv/fctranscode/restart_check.pl");

		} catch (IOException ioe) { System.out.println("Error while trying to execute restart_check.pl:"+ioe); }

		
		/* Disabled while testing other stuff
		// Run the poller, which checks the repository for new transcoding requests
		Poller pol = new Poller();
		Thread polt = new Thread(pol);
		polt.start();
		*/		

		SocketThrdServer sts = new SocketThrdServer();
		sts.listenSocket(port);
	}
}

class Poller implements Runnable{

	Poller(){
		/* Do nothing */
	}
	
	public void run (){
		try {
			while(true) {
				// Wait for 5 mins
				Thread.currentThread().sleep(300000);
				Process child = Runtime.getRuntime().exec("/Users/ecstv/fctranscode/poll.pl");
				}
		} catch (IOException ioe) { System.out.println("Error while trying to execute poll.pl:"+ioe+"\n");
		} catch (InterruptedException ie) {System.out.println("Error while trying to sleep poll thread:"+ie+"\n");
		}
	}

}
